# Freesail Developer Guide

This guide covers how to build **Agent-driven UI** applications using the **Freesail SDK**. You'll learn how to set up the architecture, run the gateway, connect agents, and build interactive UIs.

---

## 1. Core Concept

Freesail uses a **Triangle Pattern** with three independent processes:

```
┌────────────────┐    MCP Streamable HTTP     ┌──────────────────┐    A2UI SSE   ┌──────────────┐
│   AI Agent     │  ◄────────────────────────►│ Freesail Gateway │ ◄───────────► │ React App    │
│  (Orchestrator)│      Port 3000             │    (Bridge)      │   Port 3001   │ (Renderer)   │
└────────────────┘    localhost only          └──────────────────┘               └──────────────┘
```

- **Agent**: Decides *what* to show by calling MCP tools (e.g., `create_surface`, `update_components`).
- **Gateway**: Translates between MCP (agent-facing) and A2UI (UI-facing). Validates agent output against catalog schemas.
- **Frontend**: Renders A2UI JSON into React components and sends user actions back to the agent.

---

## 2. The Freesail Gateway

The Gateway is the central bridge between agents and frontends. It runs as a standalone Node.js process with **two network-facing interfaces**:

| Interface | Port | Protocol | Purpose |
|-----------|------|----------|---------|
| **Agent-facing** | 3000 (default) | MCP Streamable HTTP | Exposes tools, resources, and prompts to AI agents |
| **UI-facing** | 3001 (default) | HTTP SSE + POST | Streams A2UI updates to the frontend, receives user actions |

### Starting the Gateway

```bash
# HTTP mode (default) — agents connect via HTTP on port 3000
freesail run gateway --mcp-port 3000 --http-port 3001

# Stdio mode — agent spawns gateway as child process
freesail run gateway --mcp-mode stdio --http-port 3001
```

### CLI Options

| Option | Default | Description |
|--------|---------|-------------|
| `--config <file>` | `freesail-gateway.config.json` | Path to JSON config file |
| `--mcp-mode <mode>` | `http` | MCP transport: `http` (standalone) or `stdio` (child process) |
| `--mcp-port <port>` | `3000` | Port for MCP Streamable HTTP server (http mode only) |
| `--mcp-host <host>` | `127.0.0.1` | Bind address for MCP server (http mode only) |
| `--http-port <port>` | `3001` | Port for A2UI HTTP/SSE server |
| `--http-host <host>` | `0.0.0.0` | Bind address for A2UI HTTP/SSE server |
| `--log-file <file>` | — | Write logs to file (in addition to console) |
| `--log-level <level>` | `info` | Minimum log level: `fatal` \| `error` \| `warn` \| `info` \| `debug` |
| `--log-filter <f>` | — | Per-subsystem level override, e.g. `express:debug`. Repeatable. |

### Config File

All gateway settings can be provided via a JSON config file instead of (or alongside) CLI flags. **CLI flags take precedence over config file values.**

By default the gateway looks for `freesail-gateway.config.json` in the current working directory. Override the path with `--config`:

```bash
freesail run gateway --config /etc/freesail/gateway.json
```

A sample config file is included in the package at `node_modules/@freesail/gateway/freesail.config.sample.json`. Copy and edit it as a starting point:

```bash
cp node_modules/@freesail/gateway/freesail.config.sample.json freesail-gateway.config.json
```

**Full config file reference:**

```json
{
  "httpPort": 3001,
  "httpHost": "0.0.0.0",
  "mcpMode": "http",
  "mcpPort": 3000,
  "mcpHost": "127.0.0.1",
  "webhookUrl": "http://localhost:3002/action",
  "sessionTimeout": 1800000,
  "catalogLogDir": "/var/log/freesail/catalogs",
  "bodyLimit": "5mb",
  "tls": {
    "cert": "/path/to/cert.pem",
    "key": "/path/to/key.pem",
    "ca": "/path/to/ca.pem"
  },
  "log": {
    "level": "info",
    "file": "/var/log/freesail/gateway.log",
    "filters": {
      "express": "info",
      "mcp": "warn",
      "session": "info",
      "session.agent-surface": "debug",
      "session.client-surface": "warn"
    }
  }
}
```

All fields are optional. Omit any you don't need and the defaults will apply.

### HTTPS / TLS

By default the gateway runs over plain HTTP. To enable HTTPS, set the `tls` block in the config file pointing to your certificate and private key:

```json
{
  "tls": {
    "cert": "/etc/ssl/certs/gateway.crt",
    "key": "/etc/ssl/private/gateway.key"
  }
}
```

Both the A2UI server (port 3001) and the MCP HTTP server (port 3000) will use HTTPS when TLS is configured. The `ca` field is optional and only needed for custom certificate authorities.

### Environment Variables

The following environment variables are supported as an alternative to the config file for container/cloud deployments:

| Variable | Description |
|----------|-------------|
| `CATALOG_LOG_DIR` | Directory to write catalog prompt logs to (overridden by `catalogLogDir` in config) |
| `GATEWAY_PORT` | Used by the Vite dev proxy in the example app to point to the gateway |

### Session Timeout

By default, idle sessions are removed after **30 minutes**. Override via config file:

```json
{ "sessionTimeout": 3600000 }
```

Or keep the default and rely on the cleanup interval (1 minute) to remove stale sessions.

### Network Isolation

By default, the MCP server binds to `127.0.0.1` — only local processes can connect. The A2UI server binds to `0.0.0.0`, making it accessible from browsers. This provides network-level security without requiring authentication.

### Logging

Logs are written to the console by default. Use `--log-file` (or `log.file` in config) to mirror them to a file:

```bash
freesail run gateway --log-file logs/gateway.log
```

Control verbosity with `--log-level`:

```bash
freesail run gateway --log-level warn          # only warnings and errors
freesail run gateway --log-level debug         # maximum verbosity
```

Fine-tune individual subsystems with `--log-filter <subsystem>:<level>` (repeatable):

```bash
# Suppress routine HTTP events but keep full MCP debug output
freesail run gateway --log-level warn --log-filter mcp:debug

# Verbose agent-surface events only
freesail run gateway --log-filter session.agent-surface:debug
```

| Subsystem | Covers |
|-----------|--------|
| `express` | SSE connections, incoming actions, catalog registration |
| `mcp` | Agent MCP tool calls, session handshake |
| `session` | Surface creates/updates, data-model writes, stale-session cleanup |
| `session.agent-surface` | Downstream messages sent to agents |
| `session.client-surface` | Downstream messages sent to browser clients |

### How the Gateway Processes Requests

1. **Agent → Gateway (MCP)**: Agent calls tools like `create_surface` or `update_components`. The gateway validates the call against the catalog schema and pushes the result to the appropriate frontend session via SSE.

2. **Frontend → Gateway → Agent (Actions)**: When a user clicks a button, the frontend POSTs an action to the gateway. The gateway queues it as an MCP resource, and the agent polls for pending actions.

---

## 3. Setting Up the React Application

### Install the SDK

```bash
npm install freesail @freesail/catalogs
```

### Configure the FreesailProvider

`FreesailProvider` manages the gateway connection, catalog registration, and theming in a single component.

```tsx
import { ReactUI } from 'freesail';
import { StandardCatalog, ChatCatalog } from '@freesail/catalogs';

const CATALOGS: ReactUI.CatalogDefinition[] = [
  StandardCatalog,
  ChatCatalog,
];

function App() {
  return (
    <ReactUI.FreesailProvider
      catalogs={CATALOGS}
      theme="light"           // 'light' | 'dark' | token overrides object
    >
      <MainLayout />
    </ReactUI.FreesailProvider>
  );
}
```

The gateway URL is derived automatically from the current host on port `3001`. Override it via the `VITE_GATEWAY_URL` or `VITE_GATEWAY_PORT` environment variables, or pass `sseUrl` / `postUrl` props directly.

### Theming

Freesail ships with built-in light and dark themes. The `theme` prop on `FreesailProvider` controls which set of CSS custom properties (`--freesail-*`) is applied:

```tsx
// Light (default)
<ReactUI.FreesailProvider theme="light" catalogs={CATALOGS}>

// Dark
<ReactUI.FreesailProvider theme="dark" catalogs={CATALOGS}>

// Custom overrides (extends light defaults)
const myTheme: Partial<ReactUI.FreesailThemeTokens> = {
  primary:      '#e11d48',  // Rose 600
  primaryHover: '#be123c',  // Rose 700
  bgRaised:     '#fff1f2',  // Rose 50
  radiusMd:     '0px',      // Square corners
};
<ReactUI.FreesailProvider theme={myTheme} catalogs={CATALOGS}>
```

Token overrides merge on top of the light theme defaults. Specify only the tokens you want to change — all others keep their defaults.

See the **Theme Token Reference** in [Creating Custom Catalogs.md](./Creating%20Custom%20Catalogs.md#theme-token-reference) for the full list of available `--freesail-*` CSS custom properties with their light/dark defaults.

### Adding Surfaces

A `FreesailSurface` is a designated area that the AI agent can control.

```tsx
import { ReactUI } from 'freesail';

function MainLayout() {
  return (
    <div className="app-container">
      {/* Client-managed surface (prefix with __) */}
      <aside className="sidebar">
        <ReactUI.FreesailSurface surfaceId="__chat" />
      </aside>

      {/* Agent-created surface (alphanumeric only) */}
      <main className="content">
        <ReactUI.FreesailSurface surfaceId="workspace" />
      </main>
    </div>
  );
}
```

### Surface Naming Rules

| Type | Naming | Who creates it? | Agent permissions |
|------|--------|-----------------|-------------------|
| **Agent-managed** | Alphanumeric (e.g., `workspace`) | Agent via `create_surface` | Full control |
| **Client-managed** | Starts with `__` (e.g., `__chat`) | React app | `updateDataModel` only |

---

## 4. Building the AI Agent

The agent connects to the gateway's MCP endpoint and uses tools to drive the UI.

### Connecting to the Gateway

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const transport = new StreamableHTTPClientTransport(
  new URL('http://localhost:3000/mcp')
);

const mcpClient = new Client(
  { name: 'my-agent', version: '1.0.0' },
  { capabilities: {} }
);

await mcpClient.connect(transport);
```

### Discovering Catalogs

Before creating a surface, call `get_catalogs` to find out which component catalogs the connected client supports. Each entry contains the `catalogId` needed for `create_surface` and the full component definitions.

```typescript
const result = await mcpClient.callTool({
  name: 'get_catalogs',
  arguments: { sessionId: 'session_abc123' },
});
// result.content[0].text → JSON array: [{ catalogId, title, content }]
// catalogId is the exact string to pass to create_surface
```

If the array is empty, no catalogs are registered for this session and UI surfaces cannot be created.

### Creating a Surface

```typescript
await mcpClient.callTool({
  name: 'create_surface',
  arguments: {
    surfaceId: 'workspace',
    catalogId: 'https://freesail.ai/catalogs/standard_catalog_v1.json', // from get_catalogs
    sessionId: 'session_abc123',
  },
});
```

### Updating Components

```typescript
await mcpClient.callTool({
  name: 'update_components',
  arguments: {
    surfaceId: 'workspace',
    sessionId: 'session_abc123',
    components: [
      { id: 'root', component: 'Column', children: ['greeting'] },
      { id: 'greeting', component: 'Text', text: 'Hello from the Agent!' },
    ],
  },
});
```

---

## 5. Driving Interactivity (Data Models)

### Data Binding

Bind component properties to the data model for automatic UI updates:

```typescript
// Agent sets up components with data bindings
await mcpClient.callTool({
  name: 'update_components',
  arguments: {
    surfaceId: 'ticker',
    sessionId,
    components: [
      {
        id: 'price',
        component: 'Text',
        text: { path: '/currentPrice' },  // Binds to data model
      },
    ],
  },
});

// Set the data model
await mcpClient.callTool({
  name: 'update_data_model',
  arguments: {
    surfaceId: 'ticker',
    sessionId,
    path: '/currentPrice',
    value: '$150.00',
  },
});
```

### Real-Time Updates

Update data without re-sending the component tree:

```typescript
await mcpClient.callTool({
  name: 'update_data_model',
  arguments: {
    surfaceId: 'ticker',
    sessionId,
    path: '/currentPrice',
    value: '$155.50',
  },
});
```

---

## 6. Handling User Actions

When a user interacts with a component, the SDK sends an **Action** back through the gateway:

1. **UI Event**: User clicks a button in the browser.
2. **Action Payload**: Freesail POSTs the action to the gateway with the surface's data model.
3. **Agent Processing**: The agent picks up the action via MCP and responds.

```json
{
  "version": "v0.9",
  "action": {
    "name": "submit_form",
    "surfaceId": "workspace",
    "sourceComponentId": "submit-btn",
    "context": { "formData": "..." }
  },
  "dataModel": {
    "surfaceId": "workspace",
    "dataModel": { "items": [], "total": 99.99 }
  }
}
```

---

## 7. Running the Full Stack

The easiest way to run everything is with the provided script:

```bash
export GOOGLE_API_KEY=your-api-key
cd example && bash run-all.sh
```

This starts three independent processes:

| Process | URL | Purpose |
|---------|-----|---------|
| Gateway | `http://localhost:3001` (A2UI), `http://127.0.0.1:3000` (MCP) | Bridge between agent and UI |
| Agent | `http://localhost:3002` | AI agent with health endpoint |
| UI | `http://localhost:5173` | Vite React dev server |

---

## 8. Debugging

### Session Identification
- The gateway assigns a `sessionId` to each SSE connection.
- The React SDK attaches this ID to every HTTP POST via the `X-A2UI-Session` header.
- The agent receives the `sessionId` through a synthetic `__session_connected` action.

If you need to access the current session ID in your React components (for example, to pass it to your own backend API or a conversational agent), you can use the `useSessionId` hook:

```tsx
import { ReactUI } from 'freesail';

function SessionInfo() {
  const sessionId = ReactUI.useSessionId();
  
  if (!sessionId) return <div>Connecting to Gateway...</div>;

  return <div>Connected Session: {sessionId}</div>;
}
```

### Common Issues

| Symptom | Check |
|---------|-------|
| UI stuck on "Loading surface..." | Is the `__chat` surface being bootstrapped? Check agent logs. |
| Agent not receiving actions | Check the gateway logs for upstream messages. Verify `X-A2UI-Session` header in browser Network tab. |
| Components not rendering | Verify the `catalogId` matches a registered catalog. Check browser console for registry errors. |
| TextFields not editable in templates | Ensure the agent is using relative paths (e.g., `{ path: "name" }`) for data bindings inside ChildList templates. |

---

## 9. Best Practices

- **Surface Isolation**: Use different `surfaceId`s for different logical parts of your app.
- **Data Updates**: Use `update_data_model` to set or replace values at any path.
- **Catalog Selection**: Only provide the catalogs necessary for a surface to keep the agent focused.
- **Network Security**: In production, keep the MCP port bound to localhost and use auth for the A2UI endpoints.
